Skip to content

Copilot supported conversion to SCSS Modules#13013

Draft
andrewscfc wants to merge 101 commits intolatestfrom
andrew-scss-modules
Draft

Copilot supported conversion to SCSS Modules#13013
andrewscfc wants to merge 101 commits intolatestfrom
andrew-scss-modules

Conversation

@andrewscfc
Copy link
Copy Markdown
Contributor

@andrewscfc andrewscfc commented Jul 25, 2025

Summary

This PR presents a viable alternative to Emotion for styling; in a retro in the summer we identified that Emotion presents several pain points and currently blocks the adoption of App Router in NextJS.

The PR presents an approach utilising SASS, CSS Modules and CSS custom variables; the high level approach is as follows:

  • We create a ThemeProviderSCSSModules that imports relevant fonts and styles dependent on the chosen service, as the ThemeProvider does today
  • This wraps the existing page like the existing ThemeProvider does
  • The ThemeProviderSCSSModules imports css modules that declare suitable css custom variables that are dictated by the chosen service
  • These variables are used in module SCSS files in components to interface with the customisations the service's theme makes
  • The variables can be interfaced with via SCSS mixins; an example can be found here where we take the chosen font scale and assign font-size and line-height as controlled by the css custom variables configured by the theme.

Tech Choice Rationale

  • CSS modules allows a direct conversion of emotion styles to css module files, you are ultimately expressing raw css as you do with emotion, conversions can be reviewed with confidence.
    • Adoption of a technology such as tailwind would need the reviewer to understand tailwind and translate the converted css to that tech's best practices and features
  • SCSS and CSS custom variables allow us to replicate the Emotion theming as we have it today replicating existing theme configuration

Adoption path

  • The approach 'as-is' can co-exist with Emotion styling so could be adopted gradually
  • I would really like to automate migration via extensive use of AI tools

Limitations

  • Works in Next app and Storybook
  • Partially works in Express, you can uncomment here to play with it in Express but doesn't seem a priority)
  • Only Mundo theme implemented

Next Steps

  • Get buy in from development team to new approach
  • Implement remaining service themes (mundo was done with Copilot support, the other themes should be doable with similar automation - here is a pretty good attempt I did with AI on a learning day Scss modules remaining themes implemented with AI #13845)
  • Merge PR with MVP solution styling Curation/Subhead + ArticleLinksBlock only

Useful Links

Comment thread package.json
"puppeteer": "24.10.2",
"retry": "0.13.1",
"sass": "1.89.2",
"sass-loader": "16.0.5",
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copilot recommended packages to support SCSS

@@ -0,0 +1,223 @@
// fontFaces.module.scss
// SCSS mixins for @font-face rules, matching the fontFaces.ts constants
// Usage: @include reith-serif-regular-face; etc.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread src/app/components/ThemeProviderSCSSModules/fontFacesLazy.module.scss Outdated
@@ -0,0 +1,22 @@
// fontFamilies.module.scss
// SCSS variables for font-family stacks, matching fontFamilies.ts
// Usage: font-family: $reith-sans;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -0,0 +1,14 @@
// fontMediaQueries.module.scss
// SCSS variables for font media query breakpoints, matching fontMediaQueries.ts
// These are static values representing only the media query condition (no @media, no quotes).
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After some back and forth with Copilot it told me that you can't have constants that contain full media queries like the original file

You can get very close though with the condition being encapsulating in a variable

$values: map-get(map-get($font-sizes, $scale), $group);
font-size: map-get($values, font-size);
line-height: map-get($values, line-height);
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a really difficult conversion, the theme dictates the font size and line height so it seemed sensible for these be declared as global variables to set at runtime by JS based on the service theme. In theory the algorithm otherwise works the same as the ts version: https://github.com/bbc/simorgh/blob/latest/src/app/components/ThemeProvider/fontSizes.ts

$storm: #404040;
$success-core: #148A00;
$weather-blue: #067EB3;
$white: #FFFFFF;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$margin-below-400px: $full;
$gutter-below-600px: $full;
$margin-above-400px: $double;
$gutter-above-600px: $double;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quite a good comparison to the just CSS Modules equivalent, pretty much all the imperative logic is retained from the original: https://github.com/bbc/simorgh/blob/latest/src/app/components/ThemeProvider/spacings.ts

Comment thread src/app/components/ThemeProviderSCSSModules/palette.module.scss
Comment thread src/app/components/ThemeProviderSCSSModules/index.tsx Outdated
'brand-logo': 'var(--white)',
'brand-foreground': 'var(--ghost)',
'brand-highlight': 'var(--white)',
'brand-border': 'var(--postbox-30)',
Copy link
Copy Markdown
Contributor Author

@andrewscfc andrewscfc Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First example pulling in custom properties dynamically based on theme, this should reflect current behaviour

Comment thread src/app/components/ThemeProviderSCSSModules/themes/mundo.ts Outdated
@andrewscfc andrewscfc force-pushed the andrew-scss-modules branch from c3a0d1d to 8821093 Compare August 1, 2025 14:22
@andrewscfc andrewscfc force-pushed the andrew-scss-modules branch from 8821093 to 0a8dcd6 Compare August 1, 2025 14:27
Comment thread package.json Outdated
"jest-serializer-html": "7.1.0",
"jest-silent-reporter": "0.6.0",
"jsdom": "26.1.0",
"mini-css-extract-plugin": "2.9.2",
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot claims this enables extraction of css into seperate chunks based on usage

$ratio: if($cropped-height != 0, $cropped-width / $cropped-height, 1);

:root {
--brand-logo-ratio: #{$ratio};
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit of a stretch but in principle this algorithm will set this custom css property to refer to when putting the css together for the brand here:

const svgRatio = brandSVG && brandSVG.ratio;

@@ -0,0 +1,30 @@
@mixin build-logo(
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This meant to be roughly functional equivalent to: https://github.com/bbc/simorgh/blob/latest/src/app/components/ThemeProvider/chameleonLogos/index.tsx#L5

it compute values to use elsewhere for the brand and provides a suitable css class to use for styles

stroke: #000;
stroke-width: 0.335;
/* Optionally, set stroke via custom property or theme */
} No newline at end of file
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this needs moving to the :root declaration above to make it global for use elsewhere


.mundoLogoTheme {
@include logo.build-logo(1738, 425);
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applies mundo specific config to logo and actually executes the function, adding the suitably populated custom properties to the page

Based on: https://github.com/bbc/simorgh/blob/latest/src/app/components/ThemeProvider/chameleonLogos/mundo.tsx#L4

import '../../../fontVariants/reith.module.scss';
import './palette.module.scss';
import '../../../fontScripts/latinWithDiacritics.module.scss';
import '../../../chameleonLogos/mundoLogo.module.scss';
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quite important implementation detail here, these scss modules are imported and applied to the page, but because this is dynamically imported via loadable the theory is this css is correctly chunked with mundo only i.e. only css for the chosen theme is loaded

Comment thread src/app/components/ThemeProviderSCSSModules/themes/mundo/mundo.ts
Comment on lines +1 to +7
:root {
--brand-background: var(--postbox);
--brand-logo: var(--white);
--brand-foreground: var(--ghost);
--brand-highlight: var(--white);
--brand-border: var(--postbox-30);
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
:root {
--brand-background: var(--postbox);
--brand-logo: var(--white);
--brand-foreground: var(--ghost);
--brand-highlight: var(--white);
--brand-border: var(--postbox-30);
}

This file was moved, should be deleted

Comment on lines +1 to +7
:root {
--brand-background: var(--postbox);
--brand-logo: var(--white);
--brand-foreground: var(--ghost);
--brand-highlight: var(--white);
--brand-border: var(--postbox-30);
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

};


export default ThemeProvider; No newline at end of file
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Equivalent of

This is simplified for now and obviously creates its own Provider rather than use emotion

};
};

export default buildLogo;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Existing logic for brand svg unchanged by move to SCSS modules

import '../../../fontFaces/reith-serif-light.module.scss';
import '../../../fontVariants/reith.module.scss';
import './palette.module.scss';
import '../../../fontScripts/latinWithDiacritics.module.scss';
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quite important implementation detail here, these scss modules are imported and applied to the page, but because this is dynamically imported via loadable the theory is this css is correctly chunked with mundo only i.e. only css for the chosen theme is loaded

import '../../fontFaces/reith-serif-light.module.scss';
import '../../fontVariants/reith.module.scss';
import './palette.module.scss';
import '../../fontScripts/latinWithDiacritics.module.scss';
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quite important implementation detail here, these scss modules are imported and applied to the page, but because this is dynamically imported via loadable the theory is this css is correctly chunked with mundo only i.e. only css for the chosen theme is loaded

.h2 {
font-family: var(--sans-bold-font-family);
font-style: var(--sans-bold-font-style);
font-weight: var(--sans-bold-font-weight);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This usage font variables should be abstracted by a mixin, todo

Comment thread src/app/components/Curation/Subhead/index.module.scss Outdated
Co-authored-by: Andrew Bennett <andrew.bennett07@bbc.co.uk>
import '../../fontFaces/reith-serif-light.scss';
import '../../fontVariants/reith.scss';
import './palette.scss';
import '../../fontScripts/latinWithDiacritics.scss';
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These imports all have side-effects, defining various css global variables specific to the theme. These are used to alter styles by theme dynamically at runtime.

A quite crucial bit of config was needed here to allow these side effects to take effect.

mundo: loadable(
() => import(/* webpackChunkName: "themes-mundo" */ './mundo/mundo'),
),
};
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same dynamic import approach is utilised for themes; this allows only the relevant assets to be requested as before.

$group-3-max-width-bp: 62.9375; // 1007px / 16
$group-4-min-width-bp: 63; // 1008px / 16
$group-4-max-width-bp: 79.9375; // 1279px / 16
$group-5-min-width-bp: 80; // 1280px / 16
Copy link
Copy Markdown
Contributor Author

@andrewscfc andrewscfc Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be changed to use this utility for better readability to mirror the original https://github.com/bbc/simorgh/pull/13013/changes#diff-540a8d34d9211a4ca979d5eaef6394b41db38f899063a5519636507f5a9b0113R3

$triple: $full * 3; // 1.5rem (24px)
$quadruple: $full * 4; // 2rem (32px)
$quintuple: $full * 5; // 2.5rem (40px)
$sextuple: $full * 6; // 3rem (48px)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be changed to use this utility for better readability to mirror the original https://github.com/bbc/simorgh/pull/13013/changes#diff-540a8d34d9211a4ca979d5eaef6394b41db38f899063a5519636507f5a9b0113R3

{RenderChildrenOrError}
</PageWrapper>
</ThemeProvider>
<ThemeProviderSCSSModules service="mundo">
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hardcode to mundo for now

Comment thread webpack.config.client.js
modules: true,
importLoaders: 1,
esModule: false,
},
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This config was very trial and error to achieve, copilot suggests that:

  • modules: true is enabled to support css modules
  • importLoaders: 1 ensures files imported in css modules go through the sass loader first
  • esModule: false is needed for compatibility with style loader

This may be an area to optimise but with Express going soon it isn't a major area of concern

Comment thread webpack.config.server.js
use: ['css-loader', 'sass-loader'],
},
],
},
Copy link
Copy Markdown
Contributor Author

@andrewscfc andrewscfc Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same config as client except style-loader/MiniCssExtractPlugin is not needed as the css is not being inserted into pages on the server

@andrewscfc andrewscfc changed the title Copilot guided conversion to SCSS Modules Copilot supported conversion to SCSS Modules Mar 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant